﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace Cjc.Data.Mapping
{
	public class ExpressionTranslator : ExpressionVisitor
	{
		#region Context

		protected Expression context;
		protected IMappingContextInfo mappingContextInfo;

		#endregion

		#region Private members

		private IDictionary<Expression, Expression> replacements = new Dictionary<Expression, Expression>();
		private IDictionary<object, Expression> constants = new Dictionary<object, Expression>();
		private bool isFixup = false;

		private static IDictionary<Type, ILookup<string, MethodInfo>> staticMethods = new[]
			{
				typeof( Queryable ),
				typeof( Enumerable )
			}
			.Select( t => new { Type = t, Methods = t.GetMethods( BindingFlags.Public | BindingFlags.Static ).ToLookup<MethodInfo, string>( m => m.Name ) } )
			.ToDictionary( t => t.Type, t => t.Methods );

		#endregion

		#region Constructors

		public ExpressionTranslator( Type contextType )
			: this( Expression.Parameter( contextType, "context" ) )
		{
		}

		public ExpressionTranslator( Expression context )
		{
			this.context = context;

			if ( context != null ) this.mappingContextInfo = context.Type.GetMappingContextInfo();
		}

		#endregion

		#region Translation

		public LambdaExpression TranslateToLambda( Expression expression )
		{
			var translated = Translate( expression );
			var param = context as ParameterExpression;

			return ( param != null )
				? Expression.Lambda( translated, param )
				: Expression.Lambda( translated );
		}

		public virtual Expression Translate( Expression expression )
		{
			var resultType = expression.Type;

			isFixup = false;
			expression = base.Visit( expression );

			if ( resultType.IsGenericType )
			{
				isFixup = true;
				resultType = resultType.GetGenericArguments().First();
				var type = expression.Type.GetGenericArguments().First();

				if ( type != resultType )
				{
					// Expression result type doesn't match requested type, so create instances of requested type from what we have

					var sourceParam = Expression.Parameter( type, "s" );
					var resultParam = Expression.Parameter( resultType, "r" );
					var valueConvert = resultType.NullableType().GetTypeMapping();

					var initializer = ( valueConvert != null )
						? Expression.Lambda( Expression.Convert( sourceParam.ConvertFromData( valueConvert ), resultType ), sourceParam )
						: Visit( Expression.Lambda( GetMemberInitializer( resultParam, resultParam ), resultParam ) );

					expression = Expression.Call(
						typeof( Queryable ),
						"Select",
						new Type[] { type, resultType },
						expression,
						initializer );
				}
			}
			else if ( resultType != expression.Type )
			{
				var sourceParam = Expression.Parameter( resultType, "d" );

				expression = Expression.Invoke(
					Visit( Expression.Lambda( GetMemberInitializer( sourceParam, sourceParam ), sourceParam ) ),
					expression );
			}

			return expression;
		}

		#endregion

		#region Updates

		public Expression TranslateForUpdate( Expression expression, Type resultType )
		{
			isFixup = false;
			expression = base.Visit( expression );

			isFixup = true;
			var type = expression.Type.GetGenericArguments().First();

			if ( type != resultType )
			{
				// Expression result type doesn't match requested type, so create instances of requested type from what we have

				// Although resultType isn't the entity type, let Visit resolve it
				var entityParam = Expression.Parameter( resultType, "r" );
				var initializer = Visit( Expression.Lambda( GetMemberInitializer( entityParam, entityParam ), entityParam ) );
				var modelEntityType = typeof( ModelEntity<,> ).MakeGenericType( resultType, type );

				var sourceParam = Expression.Parameter( type, "s" );

				expression = Expression.Call(
					typeof( Queryable ),
					"Select",
					new Type[] { type, modelEntityType },
					expression,
					Expression.Lambda(
						Expression.MemberInit(
							Expression.New(
								modelEntityType.GetConstructor( new[] { resultType, type } ),
								Expression.Invoke(
									initializer,
									sourceParam
								),
								sourceParam
							)
						),
						sourceParam
					)
				);
			}

			return expression;
		}

		#endregion

		#region Initializer helpers

		private MemberInitExpression GetMemberInitializer( Expression source, Expression target )
		{
			var bindings = GetMemberBindings( source, target ).ToArray();

			if ( target.Type.IsValueType ) return Expression.MemberInit( Expression.New( target.Type ), bindings );

			// Find default constructor
			var constructor = target.Type.GetConstructor( BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, null, Type.EmptyTypes, null );

			if ( constructor != null ) return Expression.MemberInit( Expression.New( constructor ), bindings );

			// No default, so use first available and convert arguments
			constructor = target.Type.GetConstructors().First();

			var args = constructor.GetParameters().Select( p => GetConstructorArgument( source, target, p.Name, p.ParameterType ) ).ToArray();

			return Expression.MemberInit( Expression.New( constructor, args ), bindings );
		}

		private Expression GetConstructorArgument( Expression source, Expression target, string name, Type type )
		{
			var value = Visit( Expression.Property( source, name ) );
			var attributes = type.NullableType().GetCustomAttributes( true );

			var dataModel = attributes.OfType<EntityMappingAttribute>().FirstOrDefault();

			if ( dataModel != null )
			{
				var param = Expression.Property( target, name );

				return GetMemberInitializer( param, param );
			}

			if ( type.IsGenericType && typeof( IEnumerable ).IsAssignableFrom( type ) )
			{
				var param = Expression.Parameter( type.GetGenericArguments().First(), "p" );

				return Expression.Call(
					typeof( Enumerable ),
					"Select",
					new[] { value.Type.GetGenericArguments().First(), type.GetGenericArguments().First() },
					value,
					Visit( Expression.Lambda( GetMemberInitializer( param, param ), param ) ) );
			}

			var convert = attributes.OfType<TypeMappingAttribute>().FirstOrDefault();
//				?? value.GetDataConvert();

			return ( convert != null )
				? value.ConvertFromData( convert )
				: value;
		}

		private IEnumerable<MemberBinding> GetMemberBindings( Expression source, Expression target )
		{
			foreach ( var p in target.Type.GetProperties( BindingFlags.Instance | BindingFlags.Public ) )
			{
				var targetProperty = Expression.Property( target, p );

				var attributes = p.GetCustomAttributes( true );
				var group = attributes.OfType<PropertyGroupAttribute>().FirstOrDefault();

				if ( group != null )
				{
					yield return Expression.Bind( p, GetMemberInitializer( source, targetProperty ) );
				}
				else
				{
					var field = attributes.OfType<PropertyMappingAttribute>().FirstOrDefault();

					if ( field != null )
					{
						var sourceProperty = targetProperty;
						var value = Visit( sourceProperty );

						var convert = attributes.OfType<TypeMappingAttribute>().FirstOrDefault();
//							?? value.GetDataConvert();

						var converted = ( convert != null )
							? value.ConvertFromData( convert )
							: value;

						if ( p.PropertyType.IsAssignableFrom( converted.Type ) )
						{
							yield return Expression.Bind( p, converted );
						}
					}
				}
			}
		}

		#endregion

		#region Visitor overrides

		protected override Expression VisitConstant( ConstantExpression c )
		{
			var query = c.Value as Cjc.Data.Mapping.TranslatingEnumerableQuery;

			if ( query == null )
			{
				var type = c.Type.NullableType();
				var attributes = type.GetCustomAttributes( true );

				var convert = attributes.OfType<TypeMappingAttribute>().FirstOrDefault();
//					?? c.GetDataConvert();

				return ( convert != null )
					? c.ConvertToData( convert )
					: c;
			}

			if ( query.Enumerable != null )
			{
				Type publicType = GetPublicType( query.Enumerable.GetType() );
				return Expression.Constant( query.Enumerable, publicType );
			}

			return this.Visit( query.Expression );
		}

		protected override Expression VisitLambda<TDelegate>( Expression<TDelegate> lambda )
		{
			var parameters = lambda.Parameters.Cast<Expression>().ToReadOnlyCollection();
			var newParameters = this.Visit( parameters ).Cast<ParameterExpression>().ToArray();
			var body = this.Visit( lambda.Body );

			return Expression.Lambda( body, newParameters.ToArray() );
		}

		protected override Expression VisitUnary( UnaryExpression u )
		{
			var value = Visit( u.Operand );

			if ( u.NodeType == ExpressionType.Convert && u.Operand != null )
			{
				var attributes = u.Operand.Type.NullableType().GetCustomAttributes( true );

				var convert = attributes.OfType<TypeMappingAttribute>().FirstOrDefault();
//					?? value.GetDataConvert();

				if ( convert != null ) return isFixup ? value.ConvertFromData( convert ) : value.ConvertToData( convert );
			}

			return u.Update( value );
			//return base.VisitUnary( u );
		}

		protected override Expression VisitBinary( BinaryExpression node )
		{
			return node.Update( this.Visit( node.Left ), this.VisitAndConvert<LambdaExpression>( node.Conversion, "VisitBinary" ), this.Visit( node.Right ) );
		}

		protected override Expression VisitMethodCall( MethodCallExpression m )
		{
			var instance = this.Visit( m.Object );
			var source = Visit( m.Arguments );

			if ( m.Method.IsGenericMethod )
			{
				var info = FindGenericMethod( m.Method, source );
				source = FixupQuotedArgs( info, source );

				return Expression.Call( instance, info, source );
			}

			var resultType = m.Type.NullableType();
			var typeAttributes = resultType.GetCustomAttributes( true );
			var firstArg = source.FirstOrDefault();

			var convert = typeAttributes.OfType<TypeMappingAttribute>().FirstOrDefault();
//				?? firstArg.GetDataConvert();

			if ( convert != null && firstArg != null )
			{
				if ( m.Method == convert.GetConvertFromDataMethod( firstArg.Type ) ) return firstArg;
			}

			var typeArgs = GetMethodCallTypeArgs( m, source );

			#region From EnumerableVisitor via Reflector... Should figure out what it's doing at some point...

			if ( ( instance == m.Object ) && ( source == m.Arguments ) )
			{
				return m;
			}

			if ( ( m.Method.IsStatic || m.Method.DeclaringType.IsAssignableFrom( instance.Type ) ) && ArgsMatch( m.Method, source, typeArgs ) )
			{
				return Expression.Call( instance, m.Method, source );
			}

			BindingFlags flags = ( m.Method.IsStatic ? BindingFlags.Static : BindingFlags.Instance )
				| ( m.Method.IsPublic ? BindingFlags.Public : BindingFlags.NonPublic );

			MethodInfo mi = FindMethod( m.Method.DeclaringType, m.Method.Name, source, typeArgs, flags );
			source = this.FixupQuotedArgs( mi, source );
			return Expression.Call( instance, mi, source );

			#endregion
		}

		private Type[] GetMethodCallTypeArgs( MethodCallExpression m, ReadOnlyCollection<Expression> source )
		{
			if ( !m.Method.IsGenericMethod ) return null;

			var genericArgs = m.Method.GetGenericArguments();

			return genericArgs.Select( t => MapModelType( t ) ).ToArray();
		}

		private Type MapModelType( Type type )
		{
			var modelType = type.GetCustomAttributes( typeof( EntityMappingAttribute ), true ).Cast<EntityMappingAttribute>().FirstOrDefault();

			if ( modelType != null ) return GetTranslatedType( type, modelType.EntityType );

			if ( type.IsGenericType && !type.IsNullable() )
			{
				var argTypes = type.GetGenericArguments().Select( t => MapModelType( t ) ).ToArray();

				return type.GetGenericTypeDefinition().MakeGenericType( argTypes );
			}

			var baseType = type.NullableType();
			var attributes = baseType.GetCustomAttributes( true );
			var convert = attributes.OfType<TypeMappingAttribute>().FirstOrDefault();

			if ( convert == null ) return type;

			var convertedType = convert.GetConvertToDataMethod( baseType ).ReturnType;
			var isNullable = type.IsNullable();
			var isConvertedNullable = convertedType.IsNullable();

			if ( isNullable && !isConvertedNullable && convertedType != typeof( string ) ) convertedType = typeof( Nullable<> ).MakeGenericType( convertedType );
			else if ( !isNullable && isConvertedNullable ) convertedType = convertedType.NullableType();

			return convertedType;
		}

		protected override Expression VisitParameter( ParameterExpression p )
		{
			if ( replacements.ContainsKey( p ) ) return replacements[ p ];

			if ( p.Type.IsGenericType
				&& ( typeof( IQueryable ).IsAssignableFrom( p.Type ) || typeof( IEnumerable ).IsAssignableFrom( p.Type ) ) )
			{
				var typeArgs = p.Type.GetGenericArguments();
				var newTypeArgs = typeArgs.Select( t => MapModelType( t ) ).ToArray();

				if ( typeof( IQueryable ).IsAssignableFrom( p.Type ) && typeArgs[ 0 ] != newTypeArgs[ 0 ] )
				{
					// Get relation
					return replacements[ p ] = ApplyDataCriteria(
						GetQueryable( newTypeArgs[ 0 ] ),
						p.Type,
						newTypeArgs[ 0 ] );
				}

				return replacements[ p ] = Expression.Parameter(
					p.Type.GetGenericTypeDefinition().MakeGenericType( newTypeArgs ),
					typeArgs.First().Name + "_" + replacements.Count );
			}

			var type = MapModelType( p.Type );

			return ( p.Type != type )
				? replacements[ p ] = Expression.Parameter( type, p.Name + "_" + replacements.Count )
				: p;
		}

		protected override Expression VisitNew( NewExpression nex )
		{
			if ( isFixup ) return nex;

			var args = this.Visit( nex.Arguments );

			if ( args != nex.Arguments )
			{
				var constructor = nex.Constructor;
				var argsArray = args.ToArray();
				var members = ( nex.Members != null ) ? nex.Members.ToArray() : null;
				var type = nex.Constructor.DeclaringType;

				if ( type.IsGenericType )
				{
					var argTypes = argsArray.Select( e => e.Type ).ToArray();
					var genericType = type.GetGenericTypeDefinition().MakeGenericType( argTypes );
					constructor = genericType.GetConstructor( argTypes );
					members = members.Select( m => genericType.GetMember( m.Name ).First() ).ToArray();
				}

				if ( members != null )
				{
					return Expression.New( constructor /* nex.Constructor*/, args, members /* nex.Members*/ );
				}
				else
				{
					return Expression.New( constructor /* nex.Constructor*/, args );
				}
			}

			return nex;
		}

		protected override Expression VisitNewArray( NewArrayExpression na )
		{
			var exprs = this.Visit( na.Expressions );

			if ( exprs != na.Expressions )
			{
				var type = exprs.Select( e => e.Type ).FirstOrDefault() ?? na.Type;

				if ( na.NodeType == ExpressionType.NewArrayInit )
				{
					return Expression.NewArrayInit( type, exprs );
				}
				else
				{
					return Expression.NewArrayBounds( type, exprs );
				}
			}

			return na;
		}

		protected override MemberAssignment VisitMemberAssignment( MemberAssignment assignment )
		{
			var e = this.Visit( assignment.Expression );

			if ( e != assignment.Expression )
			{
				var attributes = assignment.Member.GetCustomAttributes( true );

				var convert = attributes.OfType<TypeMappingAttribute>().FirstOrDefault();
//					?? e.GetDataConvert();

				if ( convert != null ) e = e.ConvertFromData( convert );

				var group = attributes.OfType<PropertyGroupAttribute>().FirstOrDefault();

				if ( group != null )
				{
					// It's a group assignment, so initialize all fields
					return Expression.Bind(
						assignment.Member,
						!( e is MemberInitExpression ) ? GetMemberInitializer( assignment.Expression, assignment.Expression ) : e );
				}

				var relation = attributes.OfType<AssociationMappingAttribute>().FirstOrDefault();

				if ( relation != null )
				{
					// It's a relation, so wrap it in a new Select
					var resultType = assignment.Expression.Type.IsArray
						? assignment.Expression.Type.GetElementType()
						: assignment.Expression.Type.GetGenericArguments().First();

					var sourceType = e.Type.IsArray
						? e.Type.GetElementType()
						: e.Type.GetGenericArguments().First();

					var sourceParam = Expression.Parameter( resultType, "d" );
					var initializer = Visit( Expression.Lambda( GetMemberInitializer( sourceParam, sourceParam ), sourceParam ) );

					var select = Expression.Call(
						typeof( Enumerable ),
						"Select",
						new Type[] { sourceType, resultType },
						e,
						initializer );

					return Expression.Bind( assignment.Member, select );
				}

				var assignmentType = ( assignment.Member is PropertyInfo )
					? ( (PropertyInfo)assignment.Member ).PropertyType
					: null;

				// If assignment type differs from expression type, see if we can convert through model mapping
				if ( assignmentType != null && assignmentType != e.Type && e.Type == MapModelType( assignmentType ) )
				{
					return Expression.Bind( assignment.Member, Visit( GetMemberInitializer( assignment.Expression, assignment.Expression ) ) );
				}

				// If assignment type still differs from expression type, attempt to convert it
				if ( assignmentType != null && assignmentType != e.Type )
				{
					e = ConvertExpression( e, assignmentType );
				}

				return Expression.Bind( assignment.Member, e );
			}

			return assignment;
		}

		protected virtual Expression ConvertExpression( Expression expression, Type targetType )
		{
			return Expression.Convert( expression, targetType );
		}

		protected override Expression VisitMember( MemberExpression m )
		{
			var expression = this.Visit( m.Expression );

			if ( m.Expression != expression )
			{
				var attributes = m.Member.GetCustomAttributes( true );
				var group = attributes.OfType<PropertyGroupAttribute>().FirstOrDefault();

				// If DataGroup, "flatten" it
				if ( group != null ) return expression;

				var relation = attributes.OfType<AssociationMappingAttribute>().FirstOrDefault();

				// Map relation
				if ( relation != null )
				{
					var relationType = GetTranslatedRelationType( m.Type, relation.RelationType );
					var relationModelType = GetTranslatedType( m.Type, relation.RelationType );
					var properties = expression.Type.GetProperties( BindingFlags.Instance | BindingFlags.Public );

					// Find association by name first, followed by relation type (collection) and relation model type
					var property = properties.Where( p => p.Name == relation.PropertyName && ( p.PropertyType == relationType || p.PropertyType == relationModelType ) ).FirstOrDefault()
						?? properties.Where( p => p.PropertyType == relationType ).FirstOrDefault()
						?? properties.Where( p => p.PropertyType == relationModelType ).FirstOrDefault();

					var relatedItems = (Expression)Expression.MakeMemberAccess( expression, property );

					if ( m.Type.IsGenericType )
					{
//						var relateDataType = GetTranslatedType( m.Type, relation.RelationType );

						relatedItems = ApplyDataCriteria( relatedItems, m.Type, relationModelType /* relateDataType*/ );
					}

					return relatedItems;
				}

				var convert = attributes.OfType<TypeMappingAttribute>().FirstOrDefault();
				var field = attributes.OfType<PropertyMappingAttribute>().FirstOrDefault();
				var xmlField = field as XmlPropertyMappingAttribute;

				// Get data type from ToDataMethod converter if possible
				var type = ( convert != null )
					? ( convert.Type.GetMethod( convert.ToDataMethod, new[] { m.Type } )
						?? convert.Type.GetMethod( convert.ToDataMethod, Type.EmptyTypes ) ).ReturnType
					: m.Type;

				Expression memberAccess = null;

				if ( xmlField != null )
				{
					memberAccess = MakeIndirectMemberAccess( expression, xmlField.XmlField, type, m.Member.DeclaringType );
				}
				else
				{
					var name = ( field != null ) ? field.Storage : m.Member.Name;
					var member = expression.Type.GetMember( name ).FirstOrDefault();

					if ( member != null )
					{
						memberAccess = (Expression)Expression.MakeMemberAccess( expression, member );
					}
				}

				if ( memberAccess == null )
				{
					// No member found; if original was Nullable<T>.Value, just ignore it and return the expression
					if ( m.Expression.Type.IsNullable() && m.Member.Name == "Value" )
					{
						return expression;
					}
				}

				if ( memberAccess == null )
				{
					memberAccess = ( type.GetConstructor( Type.EmptyTypes ) != null || type.GetConstructors().Length == 0 )
						? (Expression)Expression.MemberInit( Expression.New( type ) )
						: Expression.Constant( null, type );
				}

				if ( !isFixup ) return memberAccess;

				if ( convert != null && !m.Type.NullableType().IsEnum ) memberAccess = memberAccess.ConvertFromData( convert, m.Type );

				return ( memberAccess.Type != m.Type && memberAccess.Type.IsValueType )
					? Expression.Convert( memberAccess, m.Type )
					: memberAccess;
			}

			return m;
		}

		private Expression ApplyDataCriteria( Expression source, Type collectionType, Type dataType )
		{
			if ( collectionType.IsGenericType )
			{
				var attributes = collectionType.GetGenericArguments().First().GetCustomAttributes( true );

				var declaringType = typeof( IQueryable<> ).IsAssignableFrom( collectionType.GetGenericTypeDefinition() )
					? typeof( Queryable )
					: typeof( Enumerable );

				foreach ( var criteria in attributes.OfType<MappingCriteriaAttribute>() )
				{
					var param = Expression.Parameter( dataType, dataType.Name );

					source = Expression.Call(
						declaringType,
						"Where",
						new Type[] { dataType },
						source,
						Expression.Lambda(
							Expression.Equal(
								Expression.Property( param, criteria.Key ),
								Expression.Constant( criteria.Value )
							),
							param
						)
					);
				}
			}

			return source;
		}

		protected Expression GetReplacement( Expression expression, Func<Expression, Expression> createReplacement )
		{
			return replacements.ContainsKey( expression )
				? constants[ expression ]
				: constants[ expression ] = createReplacement( expression );
		}

		protected Expression GetConstant<TConstant>( TConstant value, Func<TConstant, Expression> createConstant )
		{
			return constants.ContainsKey( value )
				? constants[ value ]
				: constants[ value ] = createConstant( value );
		}

		protected virtual Type GetTranslatedType( Type modelType, string translatedTypeName )
		{
			var type = mappingContextInfo.GetEntityType( translatedTypeName );

			if ( type == null ) throw new ArgumentException( string.Format( "Failed to find translated type '{0}' (from '{1}.{2}' model type)", translatedTypeName, context.Type.FullName, modelType.FullName ) );

			return type;
		}

		protected virtual Type GetTranslatedRelationType( Type modelType, string translatedTypeName )
		{
			return mappingContextInfo.GetEntityAssociationType( translatedTypeName );
		}

		protected virtual Expression GetQueryable( Type type )
		{
			return Expression.Invoke( mappingContextInfo.GetEntitiesMethod( type ), context );
		}

		protected virtual Expression MakeIndirectMemberAccess( Expression expression, string name, Type type, Type sourceType )
		{
			return null;
		}

		#endregion

		#region Based on EnumerableVisitor (via Reflector)

		public static bool ArgsMatch( MethodInfo m, ReadOnlyCollection<Expression> args, Type[] typeArgs )
		{
			ParameterInfo[] parameters = m.GetParameters();
			if ( parameters.Length != args.Count )
			{
				return false;
			}
			if ( ( !m.IsGenericMethod && ( typeArgs != null ) ) && ( typeArgs.Length > 0 ) )
			{
				return false;
			}
			if ( ( !m.IsGenericMethodDefinition && m.IsGenericMethod ) && m.ContainsGenericParameters )
			{
				m = m.GetGenericMethodDefinition();
			}
			if ( m.IsGenericMethodDefinition )
			{
				if ( ( typeArgs == null ) || ( typeArgs.Length == 0 ) )
				{
					return false;
				}
				if ( m.GetGenericArguments().Length != typeArgs.Length )
				{
					return false;
				}
				m = m.MakeGenericMethod( typeArgs );
				parameters = m.GetParameters();
			}
			int index = 0;
			int count = args.Count;
			while ( index < count )
			{
				Type parameterType = parameters[ index ].ParameterType;
				if ( parameterType == null )
				{
					return false;
				}
				if ( parameterType.IsByRef )
				{
					parameterType = parameterType.GetElementType();
				}
				Expression operand = args[ index ];
				if ( !parameterType.IsAssignableFrom( operand.Type ) )
				{
					if ( operand.NodeType == ExpressionType.Quote )
					{
						operand = ( (UnaryExpression)operand ).Operand;
					}
					if ( !parameterType.IsAssignableFrom( operand.Type ) && !parameterType.IsAssignableFrom( StripExpression( operand.Type ) ) )
					{
						return false;
					}
				}
				index++;
			}
			return true;
		}

		private MethodInfo FindGenericMethod( MethodInfo method, ReadOnlyCollection<Expression> args )
		{
			var oldTypeArgs = method.GetGenericArguments();
			var newTypeArgs = oldTypeArgs.Select( t => MapModelType( t ) ).ToArray();
			var genericDef = method.GetGenericMethodDefinition();

			if ( staticMethods.ContainsKey( genericDef.DeclaringType ) )
			{
				var methods = staticMethods[ genericDef.DeclaringType ][ genericDef.Name ]
					.Where( m => m.IsGenericMethod && m.GetGenericArguments().Length == newTypeArgs.Length )
					.ToArray();

				foreach ( var m in methods )
				{
					var typeArgs = newTypeArgs.ToArray();

					for ( var index = newTypeArgs.Length; index >= 0; --index )
					{
						var newMethod = genericDef.MakeGenericMethod( typeArgs );
						if ( ArgsMatch( newMethod, args, typeArgs ) ) return newMethod;

						if ( index > 0 ) typeArgs[ index - 1 ] = oldTypeArgs[ index - 1 ];
					}
				}

				throw new Exception( string.Format( "No {0}.{1} method found matching arguments", genericDef.DeclaringType, genericDef.Name ) );
			}

			return method;
		}

		internal static MethodInfo FindMethod( Type type, string name, ReadOnlyCollection<Expression> args, Type[] typeArgs, BindingFlags flags )
		{
			MethodInfo[] source = ( from m in type.GetMethods( flags )
									where m.Name == name
									select m ).ToArray<MethodInfo>();
			if ( source.Length == 0 )
			{
				throw new Exception( "No method on type" ); // Error.NoMethodOnType( name, type );
			}
			MethodInfo info = source.FirstOrDefault<MethodInfo>( m => ArgsMatch( m, args, typeArgs ) );
			if ( info == null )
			{
				throw new Exception( "No method on type matching arguments" ); // Error.NoMethodOnTypeMatchingArguments( name, type );
			}
			if ( typeArgs != null )
			{
				return info.MakeGenericMethod( typeArgs );
			}
			return info;
		}

		private ReadOnlyCollection<Expression> FixupQuotedArgs( MethodInfo mi, ReadOnlyCollection<Expression> argList )
		{
			ParameterInfo[] parameters = mi.GetParameters();
			if ( parameters.Length > 0 )
			{
				List<Expression> sequence = null;
				int index = 0;
				int length = parameters.Length;
				while ( index < length )
				{
					Expression expression = argList[ index ];
					ParameterInfo info = parameters[ index ];
					expression = this.FixupQuotedExpression( info.ParameterType, expression );
					if ( ( sequence == null ) && ( expression != argList[ index ] ) )
					{
						sequence = new List<Expression>( argList.Count );
						for ( int i = 0; i < index; i++ )
						{
							sequence.Add( argList[ i ] );
						}
					}
					if ( sequence != null )
					{
						sequence.Add( expression );
					}
					index++;
				}
				if ( sequence != null )
				{
					argList = sequence.ToReadOnlyCollection<Expression>();
				}
			}
			return argList;
		}

		private Expression FixupQuotedExpression( Type type, Expression expression )
		{
			Expression operand = expression;
		Label_0002:
			if ( type.IsAssignableFrom( operand.Type ) )
			{
				return operand;
			}
			if ( operand.NodeType == ExpressionType.Quote )
			{
				operand = ( (UnaryExpression)operand ).Operand;
				goto Label_0002;
			}
			if ( ( !type.IsAssignableFrom( operand.Type ) && type.IsArray ) && ( operand.NodeType == ExpressionType.NewArrayInit ) )
			{
				Type c = StripExpression( operand.Type );
				if ( !type.IsAssignableFrom( c ) )
				{
					return expression;
				}
				Type elementType = type.GetElementType();
				NewArrayExpression expression3 = (NewArrayExpression)operand;
				List<Expression> initializers = new List<Expression>( expression3.Expressions.Count );
				int num = 0;
				int count = expression3.Expressions.Count;
				while ( num < count )
				{
					initializers.Add( this.FixupQuotedExpression( elementType, expression3.Expressions[ num ] ) );
					num++;
				}
				expression = Expression.NewArrayInit( elementType, initializers );
			}
			return expression;
		}

		private static Type GetPublicType( Type t )
		{
			if ( t.IsNestedPrivate )
			{
				foreach ( Type type in t.GetInterfaces() )
				{
					if ( type.IsGenericType && ( type.GetGenericTypeDefinition() == typeof( IEnumerable<> ) ) )
					{
						return type;
					}
				}
				if ( typeof( IEnumerable ).IsAssignableFrom( t ) )
				{
					return typeof( IEnumerable );
				}
			}
			return t;
		}

		private static Type StripExpression( Type type )
		{
			bool isArray = type.IsArray;
			Type type2 = isArray ? type.GetElementType() : type;
			Type type3 = TypeHelper.FindGenericType( typeof( Expression<> ), type2 );
			if ( type3 != null )
			{
				type2 = type3.GetGenericArguments()[ 0 ];
			}
			if ( !isArray )
			{
				return type;
			}
			int arrayRank = type.GetArrayRank();
			if ( arrayRank != 1 )
			{
				return type2.MakeArrayType( arrayRank );
			}
			return type2.MakeArrayType();
		}

		#endregion
	}
}